import lazy { TypeOptions, ClassConstructor } from 'class-transformer';

const INTERNAL_PROPERTY = '__type__'

interface TypeInfo {
  constructorProvider: () => Function,
  options?: TypeOptions
}

export function QuickType(constructorProvider: () => Function, options?: TypeOptions): Function {
  return (target: Function, property: string) => rememberType(target, property, {constructorProvider, options});
}

function rememberType(target: Function, property: string, typeInfo: TypeInfo) {
  // 实测 object 比 Map 快一些
  const typeMap = target.constructor[INTERNAL_PROPERTY] =
    target.constructor[INTERNAL_PROPERTY] || {};
  typeMap[property] = typeInfo;
}

export function restorePlainToInstance<T>(cls: ClassConstructor<T>, plain: T): T {
  if (!plain) {
    return plain;
  }

  Object.setPrototypeOf(plain, cls.prototype);
  const typeMap = cls[INTERNAL_PROPERTY];
  if (!typeMap) {
    return plain;
  }

  for (const key of Object.keys(typeMap)) {
    const typeInfo: TypeInfo = typeMap[key];
    if (!typeInfo) {
      continue;
    }
    const value = plain[key];
    if (!value) {
      continue;
    }
    if (Array.isArray(value)) {
      plain[key] = [];
      for (let i = 0; i < value.length; i++) {
        if (!value[i]) {
          continue;
        }
        plain[key][i] = restorePlainToInstance(resolveConstructor(typeInfo, value[i]), value[i]);
      }
    } else {
      plain[key] = restorePlainToInstance(resolveConstructor(typeInfo, value), value);
    }
  }
  return plain;
}

function resolveConstructor<T>(typeInfo: TypeInfo, value: T): ClassConstructor<T> {
  let constructor;
  if (typeInfo.options?.discriminator) {
    constructor = typeInfo.options.discriminator.subTypes.find(
      subType => subType.name === value[typeInfo.options.discriminator.property]
    )?.value;
  }
  if (!constructor) {
    constructor = typeInfo.constructorProvider();
  }
  return constructor;
}
